/*
 * Copyright (C) 2013 Priboi Tiberiu
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.dk.animation.folding;

import com.dk.animation.R;

import android.content.Context;
import android.content.res.TypedArray;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.graphics.Paint.Style;
import android.graphics.Rect;
import android.graphics.Shader.TileMode;
import android.util.AttributeSet;
import android.view.View;
import android.view.ViewGroup;


/**
 * The folding layout where the number of folds, the anchor point and the
 * orientation of the fold can be specified. Each of these parameters can be
 * modified individually and updates and resets the fold to a default (unfolded)
 * state. The fold factor varies between 0 (completely unfolded flat image) to
 * 1.0 (completely folded, non-visible image).
 * 
 * This layout throws an exception if there is more than one child added to the
 * view. For more complicated view hierarchy's inside the folding layout, the
 * views should all be nested inside 1 parent layout.
 * 
 * This layout folds the contents of its child in real time. By applying matrix
 * transformations when drawing to canvas, the contents of the child may change
 * as the fold takes place. It is important to note that there are jagged edges
 * about the perimeter of the layout as a result of applying transformations to
 * a rectangle. This can be avoided by having the child of this layout wrap its
 * content inside a 1 pixel transparent border. This will cause an anti-aliasing
 * like effect and smoothen out the edges.
 * 
 */
public class BaseFoldingLayout extends ViewGroup {

	/*
	 * A bug was introduced in Android 4.3 that ignores changes to the Canvas
	 * state between multiple calls to super.dispatchDraw() when running with
	 * hardware acceleration. To account for this bug, a slightly different
	 * approach was taken to fold a static image whereby a bitmap of the
	 * original contents is captured and drawn in segments onto the canvas.
	 * However, this method does not permit the folding of a TextureView hosting
	 * a live camera feed which continuously updates. Furthermore, the sepia
	 * effect was removed from the bitmap variation of the demo to simplify the
	 * logic when running with this workaround."
	 */

	public static enum Orientation {
		VERTICAL, HORIZONTAL
	}

	private final String FOLDING_VIEW_EXCEPTION_MESSAGE = "Folding Layout can only 1 child at "
			+ "most";

	private final float SHADING_ALPHA = 0.8f;
	private final float SHADING_FACTOR = 0.5f;
	private final int DEPTH_CONSTANT = 1500;
	private final int NUM_OF_POLY_POINTS = 8;

	private Rect[] mFoldRectArray;

	private Matrix[] mMatrix;

	protected Orientation mOrientation = Orientation.HORIZONTAL;

	protected float mAnchorFactor = 0;
	private float mFoldFactor = 0;

	private int mNumberOfFolds = 2;

	private boolean mIsHorizontal = true;

	private int mOriginalWidth = 0;
	private int mOriginalHeight = 0;

	private float mFoldMaxWidth = 0;
	private float mFoldMaxHeight = 0;
	private float mFoldDrawWidth = 0;
	private float mFoldDrawHeight = 0;

	private boolean mIsFoldPrepared = false;
	private boolean mShouldDraw = true;

	private Paint mSolidShadow;
	private Paint mGradientShadow;
	private LinearGradient mShadowLinearGradient;
	private Matrix mShadowGradientMatrix;

	private float[] mSrc;
	private float[] mDst;

	private OnFoldListener mFoldListener;

	private float mPreviousFoldFactor = 0;

	private Bitmap mFullBitmap;
	private Rect mDstRect;

	public BaseFoldingLayout(Context context) {
		super(context);
	}

	public BaseFoldingLayout(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context, attrs);
	}

	public BaseFoldingLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		init(context, attrs);
	}

	public void init(Context context, AttributeSet attrs) {
		// now style everything!
		TypedArray ta = context.obtainStyledAttributes(attrs,
				R.styleable.FoldingMenu);
		int mFoldNumber = ta.getInt(R.styleable.FoldingMenu_foldNumber,
				mNumberOfFolds);
		if (mFoldNumber > 0 && mFoldNumber < 7) {
			mNumberOfFolds = mFoldNumber;
		} else {
			mNumberOfFolds = 2;
		}
		ta.recycle();
	}

	@Override
	protected boolean addViewInLayout(View child, int index,
			LayoutParams params, boolean preventRequestLayout) {
		throwCustomException(getChildCount());
		boolean returnValue = super.addViewInLayout(child, index, params,
				preventRequestLayout);
		return returnValue;
	}

	@Override
	public void addView(View child, int index, LayoutParams params) {
		throwCustomException(getChildCount());
		super.addView(child, index, params);
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		View child = getChildAt(0);
		measureChild(child, widthMeasureSpec, heightMeasureSpec);
		setMeasuredDimension(widthMeasureSpec, heightMeasureSpec);
	}

	@Override
	protected void onLayout(boolean changed, int l, int t, int r, int b) {
		View child = getChildAt(0);
		child.layout(0, 0, child.getMeasuredWidth(), child.getMeasuredHeight());
		updateFold();
	}

	/**
	 * The custom exception to be thrown so as to limit the number of views in
	 * this layout to at most one.
	 */
	private class NumberOfFoldingLayoutChildrenException extends
			RuntimeException {
		/**
		 * 
		 */
		private static final long serialVersionUID = 1L;

		public NumberOfFoldingLayoutChildrenException(String message) {
			super(message);
		}
	}

	/**
	 * Throws an exception if the number of views added to this layout exceeds
	 * one.
	 */
	private void throwCustomException(int numOfChildViews) {
		if (numOfChildViews == 1) {
			throw new NumberOfFoldingLayoutChildrenException(
					FOLDING_VIEW_EXCEPTION_MESSAGE);
		}
	}

	public void setFoldListener(OnFoldListener foldListener) {
		mFoldListener = foldListener;
	}

	/**
	 * Sets the fold factor of the folding view and updates all the
	 * corresponding matrices and values to account for the new fold factor.
	 * Once that is complete, it redraws itself with the new fold.
	 */
	public void setFoldFactor(float foldFactor) {
		if (foldFactor != mFoldFactor) {
			mFoldFactor = foldFactor;
			calculateMatrices();
			invalidate();
		}
	}
	


	public void setOrientation(Orientation orientation) {
		if (orientation != mOrientation) {
			mOrientation = orientation;
			updateFold();
		}
	}

	public void setAnchorFactor(float anchorFactor) {
		if (anchorFactor != mAnchorFactor) {
			mAnchorFactor = anchorFactor;
			updateFold();
		}
	}

	public void setNumberOfFolds(int numberOfFolds) {
		if (numberOfFolds != mNumberOfFolds) {
			mNumberOfFolds = numberOfFolds;
			updateFold();
		}
	}

	public float getAnchorFactor() {
		return mAnchorFactor;
	}

	public Orientation getOrientation() {
		return mOrientation;
	}

	public float getFoldFactor() {
		return mFoldFactor;
	}

	public int getNumberOfFolds() {
		return mNumberOfFolds;
	}

	private void updateFold() {
		prepareFold(mOrientation, mAnchorFactor, mNumberOfFolds);
		calculateMatrices();
		invalidate();
	}

	/**
	 * This method is called in order to update the fold's orientation, anchor
	 * point and number of folds. This creates the necessary setup in order to
	 * prepare the layout for a fold with the specified parameters. Some of the
	 * dimensions required for the folding transformation are also acquired
	 * here.
	 * 
	 * After this method is called, it will be in a completely unfolded state by
	 * default.
	 */
	private void prepareFold(Orientation orientation, float anchorFactor,
			int numberOfFolds) {

		mSrc = new float[NUM_OF_POLY_POINTS];
		mDst = new float[NUM_OF_POLY_POINTS];

		mDstRect = new Rect();

		mFoldFactor = 0;
		mPreviousFoldFactor = 0;

		mIsFoldPrepared = false;

		mSolidShadow = new Paint();
		mGradientShadow = new Paint();

		mOrientation = orientation;
		mIsHorizontal = (orientation == Orientation.HORIZONTAL);

		if (mIsHorizontal) {
			mShadowLinearGradient = new LinearGradient(0, 0, SHADING_FACTOR, 0,
					Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP);
		} else {
			mShadowLinearGradient = new LinearGradient(0, 0, 0, SHADING_FACTOR,
					Color.BLACK, Color.TRANSPARENT, TileMode.CLAMP);
		}

		mGradientShadow.setStyle(Style.FILL);
		mGradientShadow.setShader(mShadowLinearGradient);
		mShadowGradientMatrix = new Matrix();

		mAnchorFactor = anchorFactor;
		mNumberOfFolds = numberOfFolds;

		mOriginalWidth = getMeasuredWidth();
		mOriginalHeight = getMeasuredHeight();

		mFoldRectArray = new Rect[mNumberOfFolds];
		mMatrix = new Matrix[mNumberOfFolds];

		for (int x = 0; x < mNumberOfFolds; x++) {
			mMatrix[x] = new Matrix();
		}

		int h = mOriginalHeight;
		int w = mOriginalWidth;

		if (Util.IS_JBMR2 &&h!=0 &&w!=0) {
			mFullBitmap = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
			Canvas canvas = new Canvas(mFullBitmap);
			getChildAt(0).draw(canvas);
		}

		int delta = Math.round(mIsHorizontal ? ((float) w)
				/ ((float) mNumberOfFolds) : ((float) h)
				/ ((float) mNumberOfFolds));

		/*
		 * Loops through the number of folds and segments the full layout into a
		 * number of smaller equal components. If the number of folds is odd,
		 * then one of the components will be smaller than all the rest. Note
		 * that deltap below handles the calculation for an odd number of folds.
		 */
		for (int x = 0; x < mNumberOfFolds; x++) {
			if (mIsHorizontal) {
				int deltap = (x + 1) * delta > w ? w - x * delta : delta;
				mFoldRectArray[x] = new Rect(x * delta, 0, x * delta + deltap,
						h);
			} else {
				int deltap = (x + 1) * delta > h ? h - x * delta : delta;
				mFoldRectArray[x] = new Rect(0, x * delta, w, x * delta
						+ deltap);
			}
		}

		if (mIsHorizontal) {
			mFoldMaxHeight = h;
			mFoldMaxWidth = delta;
		} else {
			mFoldMaxHeight = delta;
			mFoldMaxWidth = w;
		}

		mIsFoldPrepared = true;
	}

	/*
	 * Calculates the transformation matrices used to draw each of the separate
	 * folding segments from this view.
	 */
	private void calculateMatrices() {

		mShouldDraw = true;

		if (!mIsFoldPrepared) {
			return;
		}

		/**
		 * If the fold factor is 1 than the folding view should not be seen and
		 * the canvas can be left completely empty.
		 */
		if (mFoldFactor == 1) {
			mShouldDraw = false;
			return;
		}

		if (mFoldFactor == 0 && mPreviousFoldFactor > 0
				&& mFoldListener != null) {

			mFoldListener.onEndFold();
		}

		if (mPreviousFoldFactor == 0 && mFoldFactor > 0
				&& mFoldListener != null) {

			mFoldListener.onStartFold();
		}

		mPreviousFoldFactor = mFoldFactor;

		/*
		 * Reset all the transformation matrices back to identity before
		 * computing the new transformation
		 */
		for (int x = 0; x < mNumberOfFolds; x++) {
			mMatrix[x].reset();
		}

		float cTranslationFactor = 1 - mFoldFactor;

		float translatedDistance = mIsHorizontal ? mOriginalWidth
				* cTranslationFactor : mOriginalHeight * cTranslationFactor;

		float translatedDistancePerFold = Math.round(translatedDistance
				/ mNumberOfFolds);

		/*
		 * For an odd number of folds, the rounding error may cause the
		 * translatedDistancePerFold to be grater than the max fold width or
		 * height.
		 */
		mFoldDrawWidth = mFoldMaxWidth < translatedDistancePerFold ? translatedDistancePerFold
				: mFoldMaxWidth;
		mFoldDrawHeight = mFoldMaxHeight < translatedDistancePerFold ? translatedDistancePerFold
				: mFoldMaxHeight;

		float translatedDistanceFoldSquared = translatedDistancePerFold
				* translatedDistancePerFold;

		/*
		 * Calculate the depth of the fold into the screen using pythagorean
		 * theorem.
		 */
		float depth = mIsHorizontal ? (float) Math
				.sqrt((double) (mFoldDrawWidth * mFoldDrawWidth - translatedDistanceFoldSquared))
				: (float) Math
						.sqrt((double) (mFoldDrawHeight * mFoldDrawHeight - translatedDistanceFoldSquared));

		/*
		 * The size of some object is always inversely proportional to the
		 * distance it is away from the viewpoint. The constant can be varied to
		 * to affect the amount of perspective.
		 */
		float scaleFactor = DEPTH_CONSTANT / (DEPTH_CONSTANT + depth);

		float scaledWidth, scaledHeight, bottomScaledPoint, topScaledPoint, rightScaledPoint, leftScaledPoint;

		if (mIsHorizontal) {
			scaledWidth = mFoldDrawWidth * cTranslationFactor;
			scaledHeight = mFoldDrawHeight * scaleFactor;
		} else {
			scaledWidth = mFoldDrawWidth * scaleFactor;
			scaledHeight = mFoldDrawHeight * cTranslationFactor;
		}

		topScaledPoint = (mFoldDrawHeight - scaledHeight) / 2.0f;
		bottomScaledPoint = topScaledPoint + scaledHeight;

		leftScaledPoint = (mFoldDrawWidth - scaledWidth) / 2.0f;
		rightScaledPoint = leftScaledPoint + scaledWidth;

		float anchorPoint = mIsHorizontal ? mAnchorFactor * mOriginalWidth
				: mAnchorFactor * mOriginalHeight;

		/* The fold along which the anchor point is located. */
		float midFold = mIsHorizontal ? (anchorPoint / mFoldDrawWidth)
				: anchorPoint / mFoldDrawHeight;

		mSrc[0] = 0;
		mSrc[1] = 0;
		mSrc[2] = 0;
		mSrc[3] = mFoldDrawHeight;
		mSrc[4] = mFoldDrawWidth;
		mSrc[5] = 0;
		mSrc[6] = mFoldDrawWidth;
		mSrc[7] = mFoldDrawHeight;

		/*
		 * Computes the transformation matrix for each fold using the values
		 * calculated above.
		 */
		for (int x = 0; x < mNumberOfFolds; x++) {

			boolean isEven = (x % 2 == 0);

			if (mIsHorizontal) {
				mDst[0] = (anchorPoint > x * mFoldDrawWidth) ? anchorPoint
						+ (x - midFold) * scaledWidth : anchorPoint
						- (midFold - x) * scaledWidth;
				mDst[1] = isEven ? 0 : topScaledPoint;
				mDst[2] = mDst[0];
				mDst[3] = isEven ? mFoldDrawHeight : bottomScaledPoint;
				mDst[4] = (anchorPoint > (x + 1) * mFoldDrawWidth) ? anchorPoint
						+ (x + 1 - midFold) * scaledWidth
						: anchorPoint - (midFold - x - 1) * scaledWidth;
				mDst[5] = isEven ? topScaledPoint : 0;
				mDst[6] = mDst[4];
				mDst[7] = isEven ? bottomScaledPoint : mFoldDrawHeight;

			} else {
				mDst[0] = isEven ? 0 : leftScaledPoint;
				mDst[1] = (anchorPoint > x * mFoldDrawHeight) ? anchorPoint
						+ (x - midFold) * scaledHeight : anchorPoint
						- (midFold - x) * scaledHeight;
				mDst[2] = isEven ? leftScaledPoint : 0;
				mDst[3] = (anchorPoint > (x + 1) * mFoldDrawHeight) ? anchorPoint
						+ (x + 1 - midFold) * scaledHeight
						: anchorPoint - (midFold - x - 1) * scaledHeight;
				mDst[4] = isEven ? mFoldDrawWidth : rightScaledPoint;
				mDst[5] = mDst[1];
				mDst[6] = isEven ? rightScaledPoint : mFoldDrawWidth;
				mDst[7] = mDst[3];
			}

			/*
			 * Pixel fractions are present for odd number of folds which need to
			 * be rounded off here.
			 */
			for (int y = 0; y < 8; y++) {
				mDst[y] = Math.round(mDst[y]);
			}

			/*
			 * If it so happens that any of the folds have reached a point where
			 * the width or height of that fold is 0, then nothing needs to be
			 * drawn onto the canvas because the view is essentially completely
			 * folded.
			 */
			if (mIsHorizontal) {
				if (mDst[4] <= mDst[0] || mDst[6] <= mDst[2]) {
					mShouldDraw = false;
					return;
				}
			} else {
				if (mDst[3] <= mDst[1] || mDst[7] <= mDst[5]) {
					mShouldDraw = false;
					return;
				}
			}

			/* Sets the shadow and bitmap transformation matrices. */
			mMatrix[x].setPolyToPoly(mSrc, 0, mDst, 0, NUM_OF_POLY_POINTS / 2);
		}
		/*
		 * The shadows on the folds are split into two parts: Solid shadows and
		 * gradients. Every other fold has a solid shadow which overlays the
		 * whole fold. Similarly, the folds in between these alternating folds
		 * also have an overlaying shadow. However, it is a gradient that takes
		 * up part of the fold as opposed to a solid shadow overlaying the whole
		 * fold.
		 */

		/* Solid shadow paint object. */
		int alpha = (int) (mFoldFactor * 255 * SHADING_ALPHA);

		mSolidShadow.setColor(Color.argb(alpha, 0, 0, 0));

		if (mIsHorizontal) {
			mShadowGradientMatrix.setScale(mFoldDrawWidth, 1);
			mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
		} else {
			mShadowGradientMatrix.setScale(1, mFoldDrawHeight);
			mShadowLinearGradient.setLocalMatrix(mShadowGradientMatrix);
		}

		mGradientShadow.setAlpha(alpha);
	}

	@Override
	protected void dispatchDraw(Canvas canvas) {
		/**
		 * If prepareFold has not been called or if preparation has not
		 * completed yet, then no custom drawing will take place so only need to
		 * invoke super's onDraw and return.
		 */
		if (!mIsFoldPrepared || mFoldFactor == 0) {
			super.dispatchDraw(canvas);
			return;
		}

		if (!mShouldDraw) {
			return;
		}

		Rect src;
		/*
		 * Draws the bitmaps and shadows on the canvas with the appropriate
		 * transformations.
		 */
		for (int x = 0; x < mNumberOfFolds; x++) {

			src = mFoldRectArray[x];
			/* The canvas is saved and restored for every individual fold */
			canvas.save();

			/*
			 * Concatenates the canvas with the transformation matrix for the
			 * the segment of the view corresponding to the actual image being
			 * displayed.
			 */
			canvas.concat(mMatrix[x]);
			if (Util.IS_JBMR2 ) {
				mDstRect.set(0, 0, src.width(), src.height());
				canvas.drawBitmap(mFullBitmap, src, mDstRect, null);
			} else {
				/*
				 * The same transformation matrix is used for both the shadow
				 * and the image segment. The canvas is clipped to account for
				 * the size of each fold and is translated so they are drawn in
				 * the right place. The shadow is then drawn on top of the
				 * different folds using the sametransformation matrix.
				 */
				canvas.clipRect(0, 0, src.right - src.left, src.bottom
						- src.top);

				if (mIsHorizontal) {
					canvas.translate(-src.left, 0);
				} else {
					canvas.translate(0, -src.top);
				}

				super.dispatchDraw(canvas);

				if (mIsHorizontal) {
					canvas.translate(src.left, 0);
				} else {
					canvas.translate(0, src.top);
				}
			}
			/* Draws the shadows corresponding to this specific fold. */
			if (x % 2 == 0) {
				canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight,
						mSolidShadow);
			} else {
				canvas.drawRect(0, 0, mFoldDrawWidth, mFoldDrawHeight,
						mGradientShadow);
			}

			canvas.restore();
		}
	}

}